#include "pager.h"
#include "utils.h"

struct Page {
    pPage next;
    int pinCount;
    int dirty;
    int pgno;
    char* pageContent;
};

struct PageCache {
    int capacity;       /* max count of cached page*/
    int pageCount;
    int cacheCnt;       /* current how many pages have cached*/
    pPage first;
};

struct PageManager {
    char* fName ;       /* file name*/
    int fd;
    int pageCount;      /* how many pages this file have*/
    pPageCache pCache;
};

/*
** internal function
*/
static void     __freePageCache(pPageCache ppc);
static void     __freeOnePage(pPageCache ppc, pPage pp);
static pPage    __obtainGetFromCache(pPageCache ppc, int pageno);
static pPage    __readPageFromFile(pPageManager pMgr, int pageno);

/* print info ***/
void printPM(pPageManager pMgr) {
    printf(" [x] PageManager info\n");
    printf("    pMgr->fName=%s\n", pMgr->fName);
    printf("    pMgr->fd=%d\n", pMgr->fd);
    printf("    pMgr->pageCount=%d\n", pMgr->pageCount);
    pPageCache ppc = pMgr->pCache;
    if (ppc != NULL) {
        printf("    ppc->capacity=%d\n", ppc->capacity);
        printf("    ppc->pageCount=%d\n", ppc->cacheCnt);
        if (ppc->cacheCnt > 0) { // print all cached page
            pPage tmp = ppc->first;
            while (tmp != NULL) {
                printf("    page:no=%d,pin=%d,dirty=%d\n", tmp->pgno, tmp->pinCount, tmp->dirty );
                tmp = tmp->next;
            }
        }
    } else {
        printf("    %s\n", "null page cache");
    }
}

/*
** open or create db file
*/
pPageManager openFile(const char* name) {
    pPageManager pMgr = malloc (sizeof(struct PageManager));
    if (pMgr == NULL)
        errExit("malloc PageManager");

    mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
    int flags = O_RDWR | O_CREAT;
    pMgr->fd = open(name, flags, mode);
    if (pMgr->fd == -1)
        errExit("openFile");

    pMgr->fName = name;
    pMgr->pageCount = getFileSize(pMgr->fd) / PAGE_SZ;
    pPageCache pCache = malloc(sizeof(struct PageCache));
    if (pCache == NULL)
        errExit("malloc PageCache");

    pCache->capacity = MAX_CACHE_COUNT;
    pCache->cacheCnt = 0;
    pCache->first = NULL;
    pMgr->pCache = pCache;
    return pMgr;
}

/*
**close this db file : free all cache, allocated memory
*/
void closeFile(pPageManager pMgr) {
    close(pMgr->fd);
    pPageCache ppc = pMgr->pCache;
    if (ppc != NULL) {
        __freePageCache(ppc);
    }
}

/*
** free pageCache
*/
void __freePageCache(pPageCache ppc) {
    pPage tmp = ppc->first;
    while (tmp != NULL) {
        pPage thePage = tmp;
        tmp = tmp->next;
        __freeOnePage(ppc, thePage);
    }
    free(ppc);
}

/*
** free the page in PageCache
*/
void __freeOnePage(pPageCache ppc, pPage pp) {
    // find pre and adjust chain
    pPage prePage = ppc->first;
    if (prePage == pp) { /* the first page in cache*/
        ppc->first = pp->next;
    } else {
        while (prePage != NULL && prePage->next != pp) {
            prePage = prePage->next;
        }
        prePage->next = pp->next;
    }

    ppc->cacheCnt--;

    // free the page memory
    if (pp->pageContent != NULL)
        free(pp->pageContent);
    free(pp);
}

/*
** search cache, return null if not found
*/
pPage __obtainGetFromCache(pPageCache ppc, int pageno) {
    pPage tmp = ppc->first;
    while (tmp != NULL && tmp->pgno != pageno ) {
        tmp = tmp->next;
    }
    return tmp;
}

/*
** read from file and create a page for it
*/
pPage __readPageFromFile(pPageManager pMgr, int pageno) {
    pPage pp = malloc(sizeof(struct Page));
    if (pp == NULL) errExit("malloc Page");
    char* content = malloc(PAGE_SZ);
    if (content == NULL) errExit("malloc page content");

    lseek(pMgr->fd, pageno * PAGE_SZ, SEEK_SET);
    read(pMgr->fd, content, PAGE_SZ);

    pp->next = NULL;
    pp->pinCount = 0;
    pp->dirty = 0;
    pp->pgno = pageno;
    pp->pageContent = content;
    return pp;
}

/*
** obtain a page, this page pinCount++
** routine of write a page:
**      obtainPage      ->
**      startWritePage  ->
**      flushPage       ->
**      releasePage
**
** routine of read a page:
**      obtainPage      ->
**      releasePage
*/
char* obtainPage(pPageManager pMgr, int pageno) {
    if (pageno > pMgr->pageCount)
        errExit("pageno too large");

    pPageCache ppc = pMgr->pCache;

    /* search if in cache */ 
    pPage target = __obtainGetFromCache(ppc, pageno);
    if (target != NULL) {
        printf("obtainPage: hit\n");
        target->pinCount ++;
        return target->pageContent;
    }

    printf("obtain page : not in cache \n");

    /* find a slot, read from file, then cache it */

    if (ppc->cacheCnt < ppc->capacity) {
        printf("add a new cached page\n");
        target = ppc->first;
        if (target != NULL) {
            while (target != NULL && target->next != NULL)
                target = target->next;
            target->next = __readPageFromFile(pMgr, pageno);
            target = target->next;
        } else {
            ppc->first = __readPageFromFile(pMgr, pageno);
            target = ppc->first;
        }
        target->pinCount ++;
        return target->pageContent;
    } else {
        printf("find and remove a cached page, for new page\n");
        // remove first page who's npin=0
        target = ppc->first;
        pPage targetPre = NULL;
        while (target != NULL && target->pinCount > 0) {
            targetPre = target;
            target = target->next;
        }
        if (target->pinCount > 0)
            errExit("cache full, cannot obtain");

        pPage newPage = __readPageFromFile(pMgr, pageno);
        newPage->next = target->next;
        targetPre->next = newPage;
        __freeOnePage(ppc, target);
        ppc->cacheCnt ++;
        return newPage->pageContent;
    }
}

/* 
** append a new page to file
** return the pageno
*/
int appendPage(pPageManager pMgr) {
    char* buf = calloc(PAGE_SZ, 1);
    lseek(pMgr->fd, 0, SEEK_END);
    write(pMgr->fd, buf, PAGE_SZ);
    pMgr->pageCount ++;
    return pMgr->pageCount - 1;
}

pPage __findByPgno(pPageManager pMgr, int pageno) {
    pPageCache ppc = pMgr->pCache;
    pPage target = ppc->first;
    while (target != NULL && target->pgno != pageno)
        target = target->next;
    return target;
}

/*
** declare not use this page, pinCount -1, 
** if the pinCount of one page equals zero,
** then it can be recycled
*/
void releasePage(pPageManager pMgr, int pageno) {
    pPage target = __findByPgno(pMgr, pageno);
    if (target == NULL)
        errExit("releasePage not exist\n");
    target->pinCount --;
}

/* 
** declare to write 
*/
void startWritePage(pPageManager pMgr, int pageno) {
    pPage target = __findByPgno(pMgr, pageno);
    if (target == NULL)
        errExit("writePage not exist\n");
    target->dirty = 1;
    // TODO write ahead log
}

/* 
** after do acturely write, need flush to disk 
*/
void flushPage(pPageManager pMgr, int pageno) {
    pPage target = __findByPgno(pMgr, pageno);
    if (target == NULL)
        errExit("writePage not exist\n");
    // lseek, write to file, clean flag dirty
    lseek(pMgr->fd, pageno * PAGE_SZ, SEEK_SET);
    write(pMgr->fd, target->pageContent, PAGE_SZ);
    target->dirty = 0;
}
